﻿// -----------------------------------------------------------------------
// <copyright file="InstallBam.cs" company="MSIT">
// TODO: Update copyright text.
// </copyright>
// -----------------------------------------------------------------------

namespace DeploymentCustomActions
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using Microsoft.Deployment.WindowsInstaller;
    using System.Reflection;
    using System.Data.SqlClient;
    using System.Xml.XPath;
    using System.Xml;
    using System.Diagnostics;
    using System.Globalization;
    using System.Security.Permissions;

    [Serializable]
    public class BamActivity
    {
        public string ComponentName { get; set; }
        public string Source { get; set; }
        public bool DropOnUninstall { get; set; }
        public string BamInstanceName { get; set; }
        public string BamDatabaseName { get; set; }
        //public string BamArchiveInstanceName { get; set; }
        //public string BamArchiveDatabaseName { get; set; }
        public string ActivityWindow { get; set; }
        public string ViewAccessAccounts { get; set; }
    }

    class BamActivityInstanceNameComparer : IEqualityComparer<BamActivity>
    {
        public bool Equals(BamActivity x, BamActivity y)
        {
            if (x == null) return y == null;
            if (y == null) return x == null;

            return String.Equals(x.BamInstanceName, y.BamInstanceName, StringComparison.OrdinalIgnoreCase) &&
                   String.Equals(x.BamDatabaseName, y.BamDatabaseName, StringComparison.OrdinalIgnoreCase);
        }

        public int GetHashCode(BamActivity obj)
        {
            if (obj == null) return 0;
            return obj.BamInstanceName.GetHashCode();
        }
    }
        

    /// <summary>
    /// Contains custom actions to install and uninstall Bam Activities.
    /// </summary>
    public sealed class InstallBam
    {
        private InstallBam() { }
        [CustomAction]
        public static ActionResult CreateBamExecute(Session session)
        {
            if (session == null) return ActionResult.Failure;

            LogMethodEntry(session, MethodInfo.GetCurrentMethod().Name);
            List<BamActivity> activities = ParseBamActivitiesTable(session);
            ScheduleCustomActionsForInstall(activities, session);
            ScheduleCustomActionsForUninstall(activities, session);

            return ActionResult.Success;
        }

        [EnvironmentPermissionAttribute(SecurityAction.Demand)]
        [CustomAction]
        public static ActionResult CommitCreateBam(Session session)
        {
            if (session == null) return ActionResult.Failure;

            LogMethodEntry(session, MethodInfo.GetCurrentMethod().Name);


            List<BamActivity> activities = session.CustomActionData.GetObject<List<BamActivity>>(Constants.BamActivities.BamActivitiesTable);
            string biztalkInstallPath = session.CustomActionData[Constants.BiztalkInstallPath];
            foreach (BamActivity activity in activities)
            {
                int returnValue = BmUpdateAllCommand(activity, biztalkInstallPath, session);
                if (returnValue != 0) return ActionResult.Failure;
            }
            return ActionResult.Success;
        }

        [EnvironmentPermissionAttribute(SecurityAction.Demand)]
        [CustomAction]
        public static ActionResult RollbackCreateBam(Session session)
        {
            if (session == null) return ActionResult.Failure;

            LogMethodEntry(session, MethodInfo.GetCurrentMethod().Name);

            List<BamActivity> activites = session.CustomActionData.GetObject<List<BamActivity>>(Constants.BamActivities.BamActivitiesTable);
            string biztalkInstallPath = session.CustomActionData[Constants.BiztalkInstallPath];

            foreach (var activity in activites)
            {
                List<string> activitiesFromXml = GetActivitiesForBam(activity);
                bool activityUsedToExist = session.CustomActionData
                            .GetObject<List<string>>(Constants.BamActivities.GetBamActivityExistingList(activity.BamInstanceName))
                            .Intersect(activitiesFromXml).Any();
                
                if (!activityUsedToExist)
                {
                    BmRemoveAllCommand(activity, biztalkInstallPath, session);
                }
            }

            return ActionResult.Success;
        }

        [EnvironmentPermissionAttribute(SecurityAction.Demand)]
        [CustomAction]
        public static ActionResult DeferredRemoveBam(Session session)
        {
            if (session == null) return ActionResult.Failure;

            LogMethodEntry(session, MethodInfo.GetCurrentMethod().Name);


            List<BamActivity> activities = session.CustomActionData.GetObject<List<BamActivity>>(Constants.BamActivities.BamActivitiesTable);
            string biztalkInstallPath = session.CustomActionData[Constants.BiztalkInstallPath];
            foreach (BamActivity activity in activities)
            {
                if (!activity.DropOnUninstall) continue;
                int returnValue = BmRemoveAllCommand(activity, biztalkInstallPath, session);
                if (returnValue != 0) return ActionResult.Failure;
            }
            return ActionResult.Success;
        }

        [EnvironmentPermissionAttribute(SecurityAction.Demand)]
        [CustomAction]
        public static ActionResult RollbackRemoveBam(Session session)
        {
            if (session == null) return ActionResult.Failure;

            LogMethodEntry(session, MethodInfo.GetCurrentMethod().Name);

            List<BamActivity> activites = session.CustomActionData.GetObject<List<BamActivity>>(Constants.BamActivities.BamActivitiesTable);
            string biztalkInstallPath = session.CustomActionData[Constants.BiztalkInstallPath];

            foreach (var activity in activites)
            {
                BmUpdateAllCommand(activity, biztalkInstallPath, session);
            }

            return ActionResult.Success;
        }

        private static void ScheduleCustomActionsForInstall(List<BamActivity> activities, Session session)
        {
            if (activities == null || activities.Count() == 0) return;

            List<BamActivity> activitesToInstall = activities.Join(
                                session.Components.Where(s => s.CurrentState == InstallState.Absent && s.RequestState == InstallState.Local),
                                s => s.ComponentName, s => s.Name, (x, y) => x).ToList();

            if (activitesToInstall.Count() == 0) return;

            CustomActionData customActionData = new CustomActionData();
            customActionData.AddObject(Constants.BamActivities.BamActivitiesTable, activitesToInstall);
            customActionData.Add(Constants.BiztalkInstallPath, session[Constants.BiztalkInternalInstallDir]);

            activities.Distinct(new BamActivityInstanceNameComparer()).Select(s => new
            {
                BamInstanceName = s.BamInstanceName,
                BamDatabaseName = s.BamDatabaseName,
                Activities = GetExistingActivities(s.BamInstanceName, s.BamDatabaseName),
                Backup = System.IO.Path.GetTempPath() + "\\" + Guid.NewGuid().ToString() + ".xml"
            }).ToList().ForEach(s =>
            {
                customActionData.AddObject(Constants.BamActivities.GetBamActivityBackupLocation(s.BamInstanceName), s.Backup);
                customActionData.AddObject(Constants.BamActivities.GetBamActivityExistingList(s.BamInstanceName), s.Activities);
            });

            session.DoAction("RollbackCreateBam", customActionData);
            session.DoAction("CommitCreateBam", customActionData);
        }

        private static void ScheduleCustomActionsForUninstall(List<BamActivity> activities, Session session)
        {
            if (activities == null || activities.Count() == 0) return;

            List<BamActivity> activitesToUninstall = activities.Join(
                                session.Components.Where(s => s.CurrentState == InstallState.Local && s.RequestState == InstallState.Absent),
                                s => s.ComponentName, s => s.Name, (x, y) => x).ToList();

            if (activitesToUninstall.Count() == 0) return;

            CustomActionData customActionData = new CustomActionData();
            customActionData.AddObject(Constants.BamActivities.BamActivitiesTable, activitesToUninstall);
            customActionData.Add(Constants.BiztalkInstallPath, session[Constants.BiztalkInternalInstallDir]);
                        
            session.DoAction("RollbackRemoveBam", customActionData);
            session.DoAction("DeferredRemoveBam", customActionData);
        }

        private static List<BamActivity> ParseBamActivitiesTable(Session session)
        {
            List<BamActivity> result = new List<BamActivity>();
            using (View view = session.Database.OpenView(Constants.BamActivities.Query))
            {
                view.Execute();
                foreach (Record record in view) using (record)
                {
                    string componentName = session.Format(record.GetString(Constants.BamActivities.ComponentName));
                    string source = session.Format(record.GetString(Constants.BamActivities.Source));
                    string dropOnUninstall = session.Format(record.GetString(Constants.BamActivities.DropOnUninstall));
                    string bamInstanceName = session.Format(record.GetString(Constants.BamActivities.BamInstanceName));
                    string bamDatabaseName = session.Format(record.GetString(Constants.BamActivities.BamDatabaseName));
                    //string bamArchiveInstanceName = session.Format(record.GetString(Constants.BamActivities.BamArchiveInstanceName));
                    //string bamArchiveDatabaseName = session.Format(record.GetString(Constants.BamActivities.BamArchiveDatabaseName));
                    string activityWindow = session.Format(record.GetString(Constants.BamActivities.ActivityWindow));
                    string viewAccessAccounts = session.Format(record.GetString(Constants.BamActivities.ViewAccessAccounts));

                    BamActivity activity = new BamActivity()
                    {
                        ComponentName = componentName,
                        Source = source,
                        DropOnUninstall = Boolean.Parse(dropOnUninstall),
                        BamInstanceName = bamInstanceName,
                        BamDatabaseName = bamDatabaseName,
                        //BamArchiveInstanceName = bamArchiveInstanceName,
                        //BamArchiveDatabaseName = bamArchiveDatabaseName,
                        ActivityWindow = activityWindow,
                        ViewAccessAccounts = viewAccessAccounts
                    };
                    result.Add(activity);
                }
            }
            return result;
        }
        
        private static int BmUpdateAllCommand(BamActivity activity, string biztalkInstallPath, Session session)
        {
            int returnValue = ExecuteBmExe(String.Format(CultureInfo.InvariantCulture, "update-all -DefinitionFile:\"{0}\" -Server:{1} -Database:{2}",
                    activity.Source, activity.BamInstanceName, activity.BamDatabaseName), biztalkInstallPath, session);

            if (returnValue != 0) return returnValue;

            foreach (string activityName in GetActivitiesForBam(activity))
            {
                returnValue = ExecuteBmExe(String.Format(CultureInfo.InvariantCulture, "set-activitywindow -Activity:{0} -TimeLength:{1} -TimeUnit:Day -Server:{2} -Database:{3}",
                        activityName, activity.ActivityWindow, activity.BamInstanceName, activity.BamDatabaseName), biztalkInstallPath, session);
                if (returnValue != 0) return returnValue;
            }

            foreach (string viewName in GetViewsForBam(activity))
            {
                foreach (string accountForAccess in activity.ViewAccessAccounts.Split(','))
                {
                    if (!String.IsNullOrWhiteSpace(accountForAccess))
                    {
                        ExecuteBmExe(String.Format(CultureInfo.InvariantCulture, "add-account -AccountName:\"{0}\" -View:{1} -Server:{2} -Database:{3}",
                                accountForAccess, viewName, activity.BamInstanceName, activity.BamDatabaseName), biztalkInstallPath, session);
                    }
                }
            }
            
            return 0;
        }

        private static int BmRemoveAllCommand(BamActivity activity, string biztalkInstallPath, Session session)
        {
            return ExecuteBmExe(String.Format(CultureInfo.InvariantCulture, "remove-all -DefinitionFile:\"{0}\" -Server:{1} -Database:{2}",
                    activity.Source, activity.BamInstanceName, activity.BamDatabaseName), biztalkInstallPath, session);
        }

        [EnvironmentPermissionAttribute(SecurityAction.Demand)]
        private static int ExecuteBmExe(string arguments, string biztalkInstallPath, Session session)
        {
            session.Log("\n\nExecuting Bm.exe {0}\n\n", arguments);
            Process process = null;
            try
            {
                process = new Process();
                process.StartInfo.WorkingDirectory = String.Format(CultureInfo.InvariantCulture, "{0}\\Tracking", biztalkInstallPath);
                process.StartInfo.Arguments = arguments;
                process.StartInfo.FileName = String.Format(CultureInfo.InvariantCulture, "{0}\\Tracking\\bm.exe", biztalkInstallPath);
                process.StartInfo.UseShellExecute = false;
                process.StartInfo.RedirectStandardOutput = true;
                process.StartInfo.RedirectStandardError = true;
                process.StartInfo.CreateNoWindow = true;

                process.OutputDataReceived += (x, y) => session.Log(y.Data ?? String.Empty);
                process.ErrorDataReceived += (x, y) => session.Log(y.Data ?? String.Empty);
                process.EnableRaisingEvents = true;
                process.Start();
                
                process.BeginOutputReadLine();
                process.BeginErrorReadLine();
                process.WaitForExit();
                int exitCode = process.ExitCode;

                return exitCode;
            }
            finally
            {
                process.Dispose();
            }
        }

        private static List<string> GetExistingActivities(string bamInstanceName, string bamDatabaseName)
        {
            List<string> activities = new List<string>();
            using (SqlConnection connection = new SqlConnection(String.Format(CultureInfo.InvariantCulture, Constants.BiztalkCatalogUrl, bamInstanceName, bamDatabaseName)))
            {
                connection.Open();
                using (SqlCommand command = new SqlCommand("SELECT name FROM sys.tables WHERE name LIKE 'bam_%_Active'", connection))
                {
                    SqlDataReader reader = command.ExecuteReader(System.Data.CommandBehavior.CloseConnection);
                    while (reader.Read())
                    {
                        activities.Add(reader.GetString(0).Replace("bam_", "").Replace("_Active", ""));
                    }
                }
            }
            return activities;
        }

        private static List<string> GetActivitiesForBam(BamActivity activity)
        {
            List<string> result = new List<string>();
            XPathDocument document = new XPathDocument(activity.Source);
            XPathNavigator navigator = document.CreateNavigator();

            XmlNamespaceManager ns = null;
            if (navigator.NameTable != null)
            {
                ns = new XmlNamespaceManager(navigator.NameTable);
                ns.AddNamespace("d", Constants.BamActivities.BamNamespace);
            }
            foreach (XPathNavigator iterator in navigator.Select("/d:BAMDefinition/d:Activity", ns))
            {
                result.Add(iterator.GetAttribute("Name", String.Empty));
            }
            return result;
        }

        private static List<string> GetViewsForBam(BamActivity activity)
        {
            List<string> result = new List<string>();
            XPathDocument document = new XPathDocument(activity.Source);
            XPathNavigator navigator = document.CreateNavigator();

            XmlNamespaceManager ns = null;
            if (navigator.NameTable != null)
            {
                ns = new XmlNamespaceManager(navigator.NameTable);
                ns.AddNamespace("d", Constants.BamActivities.BamNamespace);
            }
            foreach (XPathNavigator iterator in navigator.Select("/d:BAMDefinition/d:View", ns))
            {
                result.Add(iterator.GetAttribute("Name", String.Empty));
            }
            return result;
        }

        private static void LogMethodEntry(Session session, string methodName)
        {
            session.Log("Begin {0}", methodName);
        }

    }
}
